package org.datacoder.geocoding.jurisdiction

import com.vividsolutions.jts.geom.*
import java.sql.Blob
import org.hibernate.Hibernate
import org.datacoder.geocoding.*
import org.datacoder.geocoding.datastructures.geoshapes.*
import org.datacoder.util.DaoUtils

/**
 * Represents a political jurisdiction.  All other classes
 * in this package extend this class.  Jurisdictions store
 * the polygon (in fact, a JTS MultiPolygon) delineating
 * their boundaries, a name ("MN"), and a long name
 * ("Minnesota").
 */
class Jurisdiction implements Comparable {

    String name
    String longName
    Blob multiPolygonBlob

    static constraints = {
        longName(nullable:true)
        multiPolygonBlob(nullable:true)
    }
    static transients = ['hashCode', 'multiPolygon']

    /*-----------MULTIPOLYGON-----------*/
    /*
     * Grails cannot persist non-native types such as the MultiPolygon.
     * We work around this by serializing the MultiPolygon as a SQL
     * blob.  We override Grails' automatic getters and setters to
     * transparently provide this functionality.  (Business logic never
     * needs to deal with the blob directly.  (The getter getMultiPolyon()
     * builds a MultiPolygon out of the blob and returns it, and the setter
     * is similar.)
     */

    public MultiPolygon getMultiPolygon(){
        byte[] bytes = multiPolygonBlob.getBytes(1, (int)multiPolygonBlob.length())
        int pos = 0
        int numPolygons = readInt(bytes, pos)

        Polygon[] polygons = new Polygon[numPolygons]
        pos += 4
        for(int i = 0; i < numPolygons; i++){
            int numHoles = readInt(bytes, pos)
            LinearRing[] holes = new LinearRing[numHoles]
            pos += 4
            int numCoordinates = readInt(bytes, pos)
            Coordinate[] coordinates = new Coordinate[numCoordinates]
            pos += 4
            for(int j = 0; j < numCoordinates; j++){
                coordinates[j] = new Coordinate(readDouble(bytes, pos), readDouble(bytes, pos + 5))
                pos += 10
            }
            LinearRing shell = GeocodingUtils.geometryFactory.createLinearRing(coordinates)
            for(int j = 0; j < numHoles; j++){
                int numHoleCoordinates = readInt(bytes, pos)
                Coordinate[] holeCoordinates = new Coordinate[numHoleCoordinates]
                pos += 4
                for(int k = 0; k < numHoleCoordinates; k++){
                    holeCoordinates[k] = new Coordinate(readDouble(bytes, pos), readDouble(bytes, pos + 5))
                    pos += 10
                }
                holes[j] = GeocodingUtils.geometryFactory.createLinearRing(holeCoordinates)
            }
            polygons[i] = GeocodingUtils.geometryFactory.createPolygon(shell, holes)
        }
        MultiPolygon multiPolygon = GeocodingUtils.geometryFactory.createMultiPolygon(polygons)
        return multiPolygon
    }
    public void setMultiPolygon(MultiPolygon theMultiPolygon){
        MultiPolygon multiPolygon = theMultiPolygon
        Polygon[] polygons = new Polygon[theMultiPolygon.getNumGeometries()]
        LineString[][] holes = new LineString[polygons.length][]

        int len = 4
        for(int i = 0; i < polygons.length; i++){
            polygons[i] = (Polygon)theMultiPolygon.getGeometryN(i)
            len += 8
            len += 10*polygons[i].getNumPoints()
            holes[i] = new LineString[polygons[i].getNumInteriorRing()]
            for(int j = 0; j < holes[i].length; j++){
                holes[i][j] = (LineString)polygons[i].getInteriorRingN(j)
                len += 4
                len += 10*holes[i][j].getNumPoints()
            }
        }

        int pos = 0
        byte[] bytes = new byte[len]
        pos = writeInt(polygons.length, bytes, pos)
        for(int i = 0; i < polygons.length; i++){
            pos = writeInt(holes[i].length, bytes, pos)
            int numPoints = polygons[i].getExteriorRing().getNumPoints()
            pos = writeInt(numPoints, bytes, pos)
            Coordinate[] coordinates = polygons[i].getExteriorRing().getCoordinates()
            for(int j = 0; j < numPoints; j++){
                pos = writeDouble(coordinates[j].x, bytes, pos)
                pos = writeDouble(coordinates[j].y, bytes, pos)
            }
            for(int j = 0; j < holes[i].length; j++){
                int holeNumPoints = holes[i][j].getNumPoints()
                pos = writeInt(holeNumPoints, bytes, pos)
                Coordinate[] holeCoordinates = holes[i][j].getCoordinates()
                for(int k = 0; k < holeNumPoints; k++){
                    pos = writeDouble(holeCoordinates[k].x, bytes, pos)
                    pos = writeDouble(holeCoordinates[k].y, bytes, pos)
                }
            }
        }

        multiPolygonBlob = Hibernate.createBlob(bytes)
    }
    private int writeInt(int num, byte[] bytes, int pos){
        bytes[pos    ] = (byte)(num >>> 24 & 255)
        bytes[pos + 1] = (byte)(num >>> 16 & 255)
        bytes[pos + 2] = (byte)(num >>>  8 & 255)
        bytes[pos + 3] = (byte)(num        & 255)
        return pos + 4
    }
    private int readInt(byte[] bytes, int pos){
        int num = 0
        num |= ((int)bytes[pos    ] << 24) & 0xFF000000
        num |= ((int)bytes[pos + 1] << 16) & 0x00FF0000
        num |= ((int)bytes[pos + 2] <<  8) & 0x0000FF00
        num |= ((int)bytes[pos + 3]      ) & 0x000000FF
        return num
    }
    private int writeDouble(double num, byte[] bytes, int pos){
        long longRep = Double.doubleToLongBits(num)
        bytes[pos    ] = (byte)(longRep >>> 56 & 255)
        bytes[pos + 1] = (byte)(longRep >>> 48 & 255)
        bytes[pos + 2] = (byte)(longRep >>> 40 & 255)
        bytes[pos + 3] = (byte)(longRep >>> 32 & 255)
        bytes[pos + 4] = (byte)(longRep >>> 24 & 255)
        // These are always 0, so we ignore them.
        // bytes[pos + 5] = (byte)(longRep >>> 16 & 255)
        // bytes[pos + 6] = (byte)(longRep >>>  8 & 255)
        // bytes[pos + 7] = (byte)(longRep        & 255)
        return pos + 5
    }
    private double readDouble(byte[] bytes, int pos){
        long num = 0
        num |= ((long)bytes[pos    ] << 56) & 0xFF00000000000000
        num |= ((long)bytes[pos + 1] << 48) & 0x00FF000000000000
        num |= ((long)bytes[pos + 2] << 40) & 0x0000FF0000000000
        num |= ((long)bytes[pos + 3] << 32) & 0x000000FF00000000
        num |= ((long)bytes[pos + 4] << 24) & 0x00000000FF000000
        // These are always 0, so we ignore them.
        // num |= ((long)bytes[pos + 5] << 16) & 0x0000000000FF0000
        // num |= ((long)bytes[pos + 6] <<  8) & 0x000000000000FF00
        // num |= ((long)bytes[pos + 7]      ) & 0x00000000000000FF
        return Double.longBitsToDouble(num)
    }

    /*-------------GEOMETRY-------------*/
    public GeoPoint toGeoPoint(){
        GeoPoint gp = GeocodingUtils.convertGeometryToGeoPoint(this.getMultiPolygon())
        gp.juris = this
        return gp
    }
    public GeoRectangle toGeoRectangle(){
        GeoRectangle gr = GeocodingUtils.convertGeometryToGeoRectangle(this.getMultiPolygon())
        gr.juris = this
        return gr
    }
    public GeoInterval toGeoInterval(int type){
        GeoInterval gi = GeocodingUtils.convertGeometryToGeoInterval(this.getMultiPolygon(), type)
        gi.juris = this
        return gi
    }

    /*-------------METADATA-------------*/
    /**
     * Methods in Jurisdiction cannot see the id of the Jurisdiction if the
     * actual object is a subclass of Jurisdiction, rather than Jurisdiction
     * itself.  (They will see an id, but it will always be null.)  We must
     * therefore perform the hack below to find out the real id of the
     * Jurisdiction.  (This is a bug in Grails.)
     */
    public Long retrieveRealId(){
        Long id
        if(this.class == CongressionalDistrict.class){
            id = ((CongressionalDistrict)this).id
        }else if(this.class == Country.class){
            id = ((Country)this).id
        }else if(this.class == County.class){
            id = ((County)this).id
        }else if(this.class == Municipality.class){
            id = ((Municipality)this).id
        }else if(this.class == RepresentativeDistrict.class){
            id = ((RepresentativeDistrict)this).id
        }else if(this.class == SenatorialDistrict.class){
            id = ((SenatorialDistrict)this).id
        }else if(this.class == State.class){
            id = ((State)this).id
        }else if(this.class == ZipCode.class){
            id = ((ZipCode)this).id
        }else if(this.class == VotingDistrict.class){
            id = ((VotingDistrict)this).id
        }else{
            id = this.id
        }
        return id
    }
    public int compareTo(Object o){
        int returnVal
        returnVal = DaoUtils.compareClassNames(this, o)
        if(returnVal != 0){
            return returnVal
        }
        Jurisdiction otherJuris = (Jurisdiction)o
        returnVal = this.name.compareTo(otherJuris.name)
        if(returnVal == 0 && longName && otherJuris.longName){
            returnVal = longName.compareTo(otherJuris.longName)
        }
        if(returnVal == 0){
            Long id = retrieveRealId()
            if(otherJuris.id == null && id != null){
                returnVal = 1
            }else if(otherJuris.id != null && id == null){
                returnVal = -1
            }
            returnVal = hashCode() - otherJuris.hashCode()
        }
        return returnVal
    }
    private Integer hashCode = null
    public int hashCode(){
        if(hashCode == null){
            Long id = retrieveRealId()
            if(id != null){
                hashCode = id
            }else{
                hashCode = super.hashCode()
            }
        }
        return hashCode
    }
    public boolean equals(Object o){
        Long id = retrieveRealId()
        if((o.id == null && id != null) || (o.id != null && id == null)){
            return false
        }
        return (o.hashCode() == hashCode())
    }
    public static String retrieveShortName(){
        return "Juris"
    }
    public String toString(){
        com.vividsolutions.jts.geom.Point point = getMultiPolygon().getCentroid()
        return "Jurisdiction--Name: ${name}; Lat/Long: (${point.getY()}, ${point.getX()})"
    }

}

